问答平台(7),Spring Security

Spring Security

原理图

原理图-图示

简介

Spring Security 是一个专注于为 Java 应用程序提供身份认证和授权的框架,它的强大之处在于可以轻松扩展以满足自定义的需求。

特征

1
2
3
- 对身份认证和授权提供全面的、可扩展的支持。
- 防止各种攻击,如会话固定攻击、点击劫持、csrf 攻击等。
- 支持与 Servlet API、Spring MVC等 Web 技术集成。

springsecurity demo

引入依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

实体

  • User: 修改
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    public class User implements UserDetails {
    // true: 账号未过期
    @Override
    public boolean isAccountNonExpired() {
    return true;
    }

    // true: 账号未锁定
    @Override
    public boolean isAccountNonLocked() {
    return true;
    }

    // true: 凭证未过期
    @Override
    public boolean isCredentialsNonExpired() {
    return true;
    }

    // true: 账号可用
    @Override
    public boolean isEnabled() {
    return true;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
    List<GrantedAuthority> list = new ArrayList<>();
    list.add(new GrantedAuthority() {
    @Override
    public String getAuthority() {
    switch (type) {
    case 1:
    return "ADMIN";
    default:
    return "USER";
    }
    }
    });
    return list;
    }
    }

业务层

  • UserService: 修改
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Service
    public class UserService implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    public User findUserByName(String username) {
    return userMapper.selectByName(username);
    }

    @Override
    public UserDetails loadUserByUsername(String usernamme) throws UsernameNotFoundException {
    return this.findUserByName(usernamme);
    }
    }

表现层

  • HomeController: 修改
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @RequestMapping(path = "/index", method = RequestMethod.GET)
    public String getIndexPage(Model model) {
    // 认证成功后,结果会通过SecurityContextHolder存入SecurityContext中
    Object obj = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    if (obj instanceof User) {
    model.addAttribute("loginUser", obj);
    }
    return "/index";
    }

    // 拒绝访问时的提示页面
    @RequestMapping(path = "/denied", method = RequestMethod.GET)
    public String getDeniedPage() {
    return "/error/404";
    }

config

  • SecurityConfig: 新增
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserService userService;

    @Override
    public void configure(WebSecurity web) throws Exception {
    // 忽略静态资源的访问
    web.ignoring().antMatchers("/resources/**");
    }

    // AuthenticationManager: 认证的核心接口.
    // AuthenticationManagerBuilder: 用于构建AuthenticationManager对象的工具.
    // ProviderManager: AuthenticationManager接口的默认实现类.
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    // 内置的认证规则
    // auth.userDetailsService(userService).passwordEncoder(new Pbkdf2PasswordEncoder("12345"));

    // 自定义认证规则
    // AuthenticationProvider: ProviderManager持有一组AuthenticationProvider,每个AuthenticationProvider负责一种认证.
    // 委托模式: ProviderManager将认证委托给AuthenticationProvider.
    auth.authenticationProvider(new AuthenticationProvider() {
    // Authentication: 用于封装认证信息的接口,不同的实现类代表不同类型的认证信息.
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    String username = authentication.getName();
    String password = (String) authentication.getCredentials();

    User user = userService.findUserByName(username);
    if (user == null) {
    throw new UsernameNotFoundException("账号不存在!");
    }

    password = CommunityUtil.md5(password + user.getSalt());
    if (!user.getPassword().equals(password)) {
    throw new BadCredentialsException("密码不正确!");
    }

    // principal: 主要信息; credentials: 证书; authorities: 权限;
    return new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());
    }

    // 当前的AuthenticationProvider支持哪种类型的认证.
    @Override
    public boolean supports(Class<?> aClass) {
    // UsernamePasswordAuthenticationToken: Authentication接口的一个常用的实现类.
    return UsernamePasswordAuthenticationToken.class.equals(aClass);
    }
    });
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    // 登录相关配置
    http.formLogin()
    .loginPage("/loginpage")
    .loginProcessingUrl("/login")
    .successHandler(new AuthenticationSuccessHandler() {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
    response.sendRedirect(request.getContextPath() + "/index");
    }
    })
    .failureHandler(new AuthenticationFailureHandler() {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
    request.setAttribute("error", e.getMessage());
    request.getRequestDispatcher("/loginpage").forward(request, response);
    }
    });

    // 退出相关配置
    http.logout()
    .logoutUrl("/logout")
    .logoutSuccessHandler(new LogoutSuccessHandler() {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
    response.sendRedirect(request.getContextPath() + "/index");
    }
    });

    // 授权配置
    http.authorizeRequests()
    .antMatchers("/letter").hasAnyAuthority("USER", "ADMIN")
    .antMatchers("/admin").hasAnyAuthority("ADMIN")
    .and().exceptionHandling().accessDeniedPage("/denied");

    // 增加Filter,处理验证码
    http.addFilterBefore(new Filter() {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    HttpServletRequest request = (HttpServletRequest) servletRequest;
    HttpServletResponse response = (HttpServletResponse) servletResponse;
    if (request.getServletPath().equals("/login")) {
    String verifyCode = request.getParameter("verifyCode");
    if (verifyCode == null || !verifyCode.equalsIgnoreCase("1234")) {
    request.setAttribute("error", "验证码错误!");
    request.getRequestDispatcher("/loginpage").forward(request, response);
    return;
    }
    }
    // 让请求继续上下执行
    filterChain.doFilter(request, response);
    }
    }, UsernamePasswordAuthenticationFilter.class);

    // 记住我
    http.rememberMe()
    .tokenRepository(new InMemoryTokenRepositoryImpl())
    .tokenValiditySeconds(3600 * 24)
    .userDetailsService(userService);
    }
    }

重定向与转发

重定向-图示
转发-图示

页面

  • login.html: 修改(提示信息、p标签)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <form method="post" th:action="@{/login}">
    <p style="color:red;" th:text="${error}">
    <!--提示信息-->
    </p>
    <p>
    账号:<input type="text" name="username" th:value="${param.username}">
    </p>
    <p>
    密码:<input type="password" name="password" th:value="${param.password}">
    </p>
    </form>
  • index.html: 修改
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <!--欢迎信息-->
    <p th:if="${loginUser!=null}">
    欢迎你,<span th:text="${loginUser.username}"></span>!
    </p>
    <ul>
    <li><a th:href="@{/discuss}">帖子详情</a></li>
    <li><a th:href="@{/letter}">私信列表</a></li>
    <li><a th:href="@{/loginpage}">登录</a></li>
    <li>
    <form method="post" th:action="@{/logout}">
    <a href="javascript:document.forms[0].submit();">退出</a>
    </form>
    </li>
    </ul>

结果展示

记住我-图示

1
勾选记住我,登录后关闭浏览器,重启浏览器。

参考资料


问答平台(7),Spring Security
https://lcf163.github.io/2020/06/11/问答平台(7),Spring-Security/
作者
乘风的小站
发布于
2020年6月11日
许可协议