【java】【SpringSecurity】SpringSecurity在MVC项目中的应用

SpringSecurity用户类

/**
 * SpringSecurity中的用户实体类
 *
 */
public class SecurityUser implements UserDetails {
    private static final long serialVersionUID = 1L;
    
    private final String uid;
    private final String username;
    private final String password;
    private final boolean enabled;
    private final Collection<? extends GrantedAuthority> authorities;

    public SecurityUser(
            String uid,
            String username,
            String password,
            boolean enabled,
            Collection<? extends GrantedAuthority> authorities) {
        this.uid = uid;
        this.username = username;
        this.password = password;
        this.enabled = enabled;
        this.authorities = authorities;
    }

    /**
     * 返回分配给用户的角色列表
     *
     * @return
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }
    @JsonIgnore
    public String getUid() {
        return uid;
    }
    @JsonIgnore
    @Override
    public String getPassword() {
        return password;
    }
    @Override
    public String getUsername() {
        return username;
    }
    /**
     * 账户是否激活
     *
     * @return
     */
    @JsonIgnore
    @Override
    public boolean isEnabled() {
        return enabled;
    }
    /**
     * 账户是否未过期
     *
     * @return
     */
    @JsonIgnore
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    /**
     * 账户是否未锁定
     *
     * @return
     */
    @JsonIgnore
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    /**
     * 密码是否未过期
     *
     * @return
     */
    @JsonIgnore
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
}
  • boolean enabled 是否可用
  • boolean accountNonExpired 账户是否失效
  • boolean credentialsNonExpired 密码是否失效
  • boolean accountNonLocked 账户是否锁定
/**
 * SpringSecurity用户工厂类
 *
 */
public final class SecurityUserFactory {

    private SecurityUserFactory() {
    }

    /**
     * 通过管理员Admin,生成一个SpringSecurity用户
     *
     * @param admin
     * @return
     */
    public static SecurityUser create(Admin admin) {
        boolean enabled = admin.getStatus() == EStatus.ENABLE;
        return new SecurityUser(
                admin.getUid(),
                admin.getUserName(),
                admin.getPassWord(),
                enabled,
                mapToGrantedAuthorities(admin.getRoleNames())
        );
    }

    private static List<GrantedAuthority> mapToGrantedAuthorities(List<String> authorities) {
        return authorities.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    }

}

在工厂类SecurityUserFactory中通过管理员Admin生成SpringSecurity用户,并判断用户是否处于正常状态

注销功能

注意:一旦开启了csrf防护功能,logout处理器便只支持POST请求方式了!修改header.jsp中注销请求:

<form action="${pageContext.request.contextPath}/logout" method="post">
    <security:csrfInput/>
    <input type="submit" value="注销">
</form>

记住我功能

流程分析

上一篇入门篇中,分析源码到认证成功时调用了AbstractRememberMeServices#loginSuccess方法

// AbstractAuthenticationProcessingFilter.java
// 登陆成功,调用rememberMeServices.loginSuccess
this.rememberMeServices.loginSuccess(request, response, authResult);
public abstract class AbstractRememberMeServices implements RememberMeServices, InitializingBean, LogoutHandler {
   
    public final void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
    	// 是否勾选记住我
    	// this.parameter : private String parameter = "remember-me";
        if (!this.rememberMeRequested(request, this.parameter)) {
            this.logger.debug("Remember-me login not requested.");
        } else {
        	// 勾选,从这里进入PersistentTokenBasedRememberMeServices.onLoginSuccess方法
            this.onLoginSuccess(request, response, successfulAuthentication);
        }
    }

	protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
        if (this.alwaysRemember) {
            return true;
        } else {
        	// 提交的属性名必须叫"remember-me"
            String paramValue = request.getParameter(parameter);
            // 属性的值可以为 : true, on, yes, 1。
            if (paramValue != null && (paramValue.equalsIgnoreCase("true") || paramValue.equalsIgnoreCase("on") || paramValue.equalsIgnoreCase("yes") || paramValue.equals("1"))) {
                return true;
            } else {
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug("Did not send remember-me cookie (principal did not set parameter '" + parameter + "')");
                }

                return false;
            }
        }
    }

继续进入PersistentTokenBasedRememberMeServices#onLoginSuccess方法

public class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices {

    protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
    	// 获取用户名
        String username = successfulAuthentication.getName();
        this.logger.debug("Creating new persistent login for user " + username);
        // 创建记住我的token
        PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, this.generateSeriesData(), this.generateTokenData(), new Date());

        try {
        	// 将token持久化到数据库中
            this.tokenRepository.createNewToken(persistentToken);
            // 将token写入浏览器Cookie中
            this.addCookie(persistentToken, request, response);
        } catch (Exception var7) {
            this.logger.error("Failed to save persistent token ", var7);
        }

    }
}

页面代码

<%@taglib prefix="security" "uri='http://www.springframework.org/ security /tags"%>

<form action="${pageContext.request.contextPath}/login" method="post">
	<!--在认证from表单内携带token,需要在文件头添加SpringSecurity标签库-->
	<!--<security:csrfInput/>-->
	<div class="form-group has-feedback">
		<input type="text" name="username" class="form-control" laceholder="用户名">
		<span class="glyphicon glyphicon-envelope form-control-feedback"></span>
		</div>
		<div class="form-group has-feedback">
			<input type="password" name="password" class="form-control" placeholder="密码">
			<span class="glyphicon glyphicon-lock form-control-feedback"></span>
		</div> 
		<div>
			<div>
				<!--name和value属性不能写错-->
				<label><input type="checkbox" name="remember-me" value="true"/>记住我,下次自动登录</label>
			</div>
		</div>
		<div class="col-xs-4">
			<button type="submit" class="btn btn-primary btn-block btn-flat">登录</button>
		</div>
	<!-- /.col -->
	</div>
</form>

这是因为remember me功能使用的过滤器RememberMeAuthenticationFilter默认是不开启的!

开启remember me过滤器

<security:http auto-config="true" use-expressions="true">
	...
	<!--开启remember-me过滤器,设置token存储时间为60s-->
	</security:remember-me token-validity-seconds="60"/>
</security:http>

remember me安全性分析

记住我功能方便是大家看得见的,但是安全性却令人担忧。因为Cookie毕竟是保存在客户端的,很容易盗取,而且 Cookie的值还与用户名、密码这些敏感数据相关,虽然加密了,但是将敏感信息存在客户端,还是不太安全。那么 这就要提醒喜欢使用此功能的,用完网站要及时手动退出登录,清空认证信息

此外,SpringSecurity还提供了remember me的另一种相对更安全的实现机制 :在客户端的cookie中,仅保存一个 无意义的加密串(与用户名、密码等敏感数据无关),然后在数据库中保存该加密串-用户信息的对应关系,自动登录 时,用cookie中的加密串,到数据库中验证,如果通过,自动登录才算通过。

remember me信息的持久化

创建persistent_logins表,表名称和字段都是固定的,不能修改

CREATE TABLE `persistent_logins` (
	`username` varchar(64) NOT NULL,
	`series` varchar(64) NOT NULL,
	`token` varchar(64) NOT NULL,
	`last_used` timestamp NOT NULL,
	PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

修改spring-security.xml

<!--
开启remember me过滤器,
data-source-ref="dataSource" 指定数据库连接池
token-validity-seconds="60" 设置token存储时间为60秒 可省略
remember-me-parameter="remember-me" 指定记住的参数名 可省略
-->
<security:remember-me data-source-ref="dataSource"
token-validity-seconds="60" remember-me-parameter="remember-me"/>

显示当前认证信息

header.jsp中找到页面头部最右侧图片处添加如下信息:

<span class="hidden-xs">
	<security:authentication property="principal.username" />
</span>
或者
<span class="hidden-xs">
	<security:authentication property="name" />
</span>

在页面控制资源权限

<security:authorize access="hasAnyRole('ROLE_ADMIN')">
	<!--只有ROLE_ADMIN权限的用户可见标签里的内容-->
</ecurity:authorize>

<security:authorize access="hasAnyRole('ROLE_ADMIN', 'ROLE_USER')">
	<!--有ROLE_ADMIN或者ROLE_USER权限的用户都可见标签里的内容-->
</ecurity:authorize>

授权操作

IOC容器介绍

在这里插入图片描述
我们的spring-security.xml需要放到父容器中被保护起来,不能放到子容器中被直接访问

说明:SpringSecurity可以通过注解的方式来控制类或者方法的访问权限。注解需要对应的注解支持,若注解放在 controller类中,对应注解支持应该放在mvc配置文件中,因为controller类是有mvc配置文件扫描并创建的,同 理,注解放在service类中,对应注解支持应该放在spring配置文件中。由于我们现在是模拟业务操作,并没有 service业务代码,所以就把注解放在controller类中了。

开启授权的注解支持

在服务器端我们可以通过Spring security提供的注解对方法来进行权限控制。Spring Security在方法的权限控制上
支持三种类型的注解,JSR-250注解、@Secured注解和支持表达式的注解,这三种注解默认都是没有启用的,需要
单独通过global-method-security元素的对应属性进行启用
三类注解,但实际开发中,用一类即可!
方法一:
spring-security.xml添加

<!--
开启权限控制注解支持
jsr250-annotations="enabled"    表示支持jsr250-api的注解,需要jsr250-api的jar包
pre-post-annotations="enabled"  表示支持spring表达式注解
secured-annotations="enabled"   这才是SpringSecurity提供的注解
-->
<!--这里使用JSR-250注解-->
<security:global-method-security jsr250-annotations="enabled"
pre-post-annotations="enabled"
secured-annotations="enabled"/>

方法二:
注解开启
@EnableGlobalMethodSecurity :Spring Security默认是禁用注解的,要想开启注解,需要在继承
WebSecurityConfigurerAdapter的类上加@EnableGlobalMethodSecurity注解,并在该类中将
AuthenticationManager定义为Bean。这个再Springboot项目中再学习
在这里插入图片描述
JSR-250注解简单介绍
pom.xml文件中需要导入依赖

  • @RolesAllowed表示访问对应方法时所应该具有的角色
    示例:
    @RolesAllowed({“USER”, “ADMIN”})
    该方法只要具有"USER", "ADMIN"任意一种权限就可以访问。这里可以省
    略前缀ROLE_,实际的权限可能是ROLE_ADMIN
  • @PermitAll表示允许所有的角色进行访问,也就是说不进行权限控制
  • @DenyAll是和PermitAll相反的,表示无论什么角色都不能访问

在注解支持对应类或者方法上添加注解

@Controller
@RequestMapping("/product")
public class ProductController {
    @Autowired
    private IProductService productService;

    //查询全部产品
    @RequestMapping("/findAll.do")
    @RolesAllowed("ADMIN")
    public ModelAndView findAll() throws Exception {
        ModelAndView mv = new ModelAndView();
        List<Product> ps = productService.findAll();
        mv.addObject("productList", ps);
        mv.setViewName("product-list1");
        return mv;
    }
}

也可以使用多种注解的方式

@Controller
@RequestMapping("/product")
public class ProductController {

    //@Secured({"ROLE_PRODUCT","ROLE_ADMIN"})//springSecurity内部制定的注解
    //@RolesAllowed({"ROLE_PRODUCT","ROLE_ADMIN"})//jsr250注解
    @PreAuthorize("hasAnyAuthority('ROLE_PRODUCT','ROLE_ADMIN')")//spring的el表达式注解
    @RequestMapping("/findAll")
    public String findAll(){
        return "product-list";
    }
}

权限不足异常处理

403异常
方式一:在spring-security.xml配置文件中处理

<security:http auto-config="true" use-expressions="true">
	...
	<!--403异常处理-->
	<security:access-denied-handler error-page="/403.jsp"/>
</security:http>

方式二:在web.xml中处理

<error-page>
    <error-code>403</error-code>
    <location>/403.jsp</location>
</error-page>

方式三:编写异常处理器
拦截器和过滤器的区别

  • 拦截器:可以在Spring中进行使用
  • 过滤器:只能在web.xml中进行配置,也就是只能在web工程中使用
    或者我们可以实现一个Spring给我们提供好的接口
@Component
public class HandlerControllerException implements HandlerExceptionResolver {
    /**
     * @param httpServletRequest
     * @param httpServletResponse
     * @param o  出现异常的对象
     * @param e  出现的异常信息
     * @return   ModelAndView
     */
    @Override
    public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) {
        ModelAndView mv = new ModelAndView();
        //将异常信息放入request域中,基本不用
        mv.addObject("errorMsg", e.getMessage());
        //指定不同异常跳转的页面
        if(e instanceof AccessDeniedException){
            mv.setViewName("redirect:/403.jsp");
        }else {
            mv.setViewName("redirect:/500.jsp");
        }
        return mv;
    }
}

下面一个更简单的方式,通过注解就相当于我们实现了 HandlerExceptionResolver

@ControllerAdvice
public class HandlerControllerAdvice{
    @ExceptionHandler(AccessDeniedException.class)
    public String handlerException(){
        return "redirect:/403.jsp";
    }
    @ExceptionHandler(RuntimeException.class)
    public String runtimeHandlerException(){
        return "redirect:/500.jsp";
    }
}

学习视频

【黑马程序员】手把手教你精通新版SpringSecurity

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值